/**
 *
 */
package com.honeybees.framework.mybatis.interceptor;

import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.ibatis.builder.StaticSqlSource;
import org.apache.ibatis.builder.annotation.ProviderSqlSource;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.scripting.defaults.RawSqlSource;
import org.apache.ibatis.scripting.xmltags.DynamicSqlSource;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.honeybees.framework.common.util.Utils;
import com.honeybees.framework.mybatis.sqlsource.AbstractPageSqlSource;
import com.honeybees.framework.mybatis.sqlsource.PageDynamicSqlSource;
import com.honeybees.framework.mybatis.sqlsource.PageProviderSqlSource;
import com.honeybees.framework.mybatis.sqlsource.PageRawSqlSource;
import com.honeybees.framework.mybatis.sqlsource.PageStaticSqlSource;
import com.honeybees.framework.mybatis.util.MappedStatementUtils;
import com.honeybees.framework.spring.mvc.model.ModelSupport;

/**
 * <dl>
 * <dt><b> MyBaits分页拦截器 </b></dt>
 * <p>
 * <dd>实现MyBaits提供的拦截器接口，编写我们自己的分页实现，原理就是拦截底层JDBC操作相关的Statement对象，
 * 把前端的分页参数，如当前记录索引和每页大小，通过拦截器注入到SQL语句中 ，即在SQL执行之前通过分页参数重新生成分页SQL，而具体的分页SQL实现是分离到Dialect接口中去。</dd>
 * <p>
 * <dd>请将MyBatis的拦截器配置到Mybatis的全局配置文件(mybatisConfig.xml)中。</dd>
 * </dl>
 * <p>
 * Copyright (C) All rights reserved.
 * </p>
 *
 * @author 李远明
 * @since 2014-10-19 16:40:59 新建
 */
@Intercepts({@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class,
        RowBounds.class, ResultHandler.class})})
public final class PaginationInterceptor implements Interceptor {

    /**
     * 日志对象
     */
    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    /**
     * 缓存已经处理为包含Count查询的 MappedStatement
     */
    private static final Map<String, MappedStatement> COUNT_MAPPED_STATEMENT_MAP = new ConcurrentHashMap<String, MappedStatement>();

    /**
     * <dl>
     * <dt><b> 对 Executor 的行为进行拦截 </b></dt>
     * <p>
     * <dd>MyBatis真正生成Statement并执行SQL的语句是StatementHandler接口的某个实现。</dd>
     * <p>
     * <dd>MyBatis支持对 Executor、StatementHandler、ParameterHandler 和 ResultSetHandler 进行拦截，
     * 也就是说会对这4种对象进行代理。</dd>
     * </dl>
     *
     * @param invocation {@linkplain Invocation 入参，包括拦截参数、拦截的方法、拦截的对象}
     * @return 返回值 List
     * @throws Throwable 可能发生的异常
     * @author 李远明
     * @version 2015-10-23 15:35:41
     * @version 2016-07-22 10:37:03
     * 修改/升级分页查询拦截方法：如果是分页查询，则在查询数据之前，用一个查询总条数的MappedStatement替换当前MappedStatement，查询到数据总条数之后，再还原MappedStatement，……
     * @version 2016-07-27 11:27:47 删除在本方法中获取数据库方言的代码。
     * @see Interceptor#intercept(Invocation)
     * @see Executor#query(MappedStatement, Object, RowBounds, ResultHandler)
     * @see com.honeybees.framework.mybatis.MyBatisDaoSupport#selectPage(String, Object)
     */
    @Override
    public Object intercept(Invocation invocation) throws Throwable {

        Object[] args = invocation.getArgs(); // 四个参数

        RowBounds rowBounds = (RowBounds) args[2]; // 保存RowBounds状态

        // 如果 RowBounds 等于默认的对象，则表示不分页；直接跳转到 target（StatementHandler的实现类或实现类的子类） 中执行。
        if (rowBounds == null || rowBounds == RowBounds.DEFAULT) {
            return invocation.proceed();
        }

        this.logger.debug("intercept 开始分页查询");

        MappedStatement ms = (MappedStatement) args[0]; // 原始的MappedStatement
        Object parameterObject = args[1]; // 查询参数

        // 判断并处理 SqlSource 为 PageSqlSource
        if (!isPageSqlSource(ms)) {
            processMappedStatement(ms);
        }

        // 忽略RowBounds：告诉MyBatis不分页（不再对结果进行加工，否则会进行MyBatis自带的内存分页）
        args[2] = RowBounds.DEFAULT;

        // ------------------------- 分页查询 -------------------------
        args[0] = COUNT_MAPPED_STATEMENT_MAP.get(ms.getId()); // XXX 替换 MappedStatement

        // 先查询数据总条数
        Object resultCount = invocation.proceed();

        @SuppressWarnings("rawtypes")
        int count = Utils.safeToInt(((List) resultCount).get(0));

        this.logger.debug("intercept totalCount: {}", count);

        // XXX parameterObject 必须是 ModelSupport 的子类，MyBatisDaoSupport.selectPage 方法中有校验
        ModelSupport model = (ModelSupport) parameterObject;
        if (count == 0) { // XXX 如果数据总条数为0，则不再分页查询，直接返回空List
            model.setTotalCount(0);
            return Collections.emptyList();
        } else { // XXX 如果数据总条数不为0，则进行分页查询
            model.setTotalCount(count);

            args[0] = ms; // XXX 还原 MappedStatement

            Object resultList = invocation.proceed(); // 执行分页查询
            return resultList;
        }
    }

    /**
     * <dl>
     * <dt><b> 判断当前 MappedStatement 包含的SqlSource是否已经处理为支持分页功能的 SqlSource </b></dt>
     * <p>
     * <dd>功能描述</dd>
     * </dl>
     *
     * @param ms 已解析的SQL语句
     * @return true/false
     * @author 李远明
     * @version 2016年7月21日 下午3:14:18
     */
    private boolean isPageSqlSource(MappedStatement ms) {
        SqlSource sqlSource = ms.getSqlSource();
        return (sqlSource instanceof AbstractPageSqlSource);
    }

    /**
     * <dl>
     * <dt><b> 修改SqlSource </b></dt>
     * <p>
     * <dd>将 MappedStatement 中的 sqlSource 替换为支持分页的 sqlSource；而用于分页时，查询数据总条数和分页查询的SQL语句，则在查询执行时，通过
     * MappedStatement.getBoundSql 和 AbstractPageSqlSource.getBoundSql 方法根据条件获取。</dd>
     * <p>
     * </dl>
     *
     * @param ms 已解析的SQL语句
     * @see MappedStatement#getBoundSql(Object)
     * @see AbstractPageSqlSource#getBoundSql(Object)
     */
    private void processMappedStatement(MappedStatement ms) {
        SqlSource sqlSource = ms.getSqlSource();
        SqlSource pageSqlSource;
        if (sqlSource instanceof StaticSqlSource) {
            pageSqlSource = new PageStaticSqlSource((StaticSqlSource) sqlSource);
        } else if (sqlSource instanceof RawSqlSource) {
            pageSqlSource = new PageRawSqlSource((RawSqlSource) sqlSource);
        } else if (sqlSource instanceof ProviderSqlSource) {
            pageSqlSource = new PageProviderSqlSource((ProviderSqlSource) sqlSource);
        } else if (sqlSource instanceof DynamicSqlSource) {
            pageSqlSource = new PageDynamicSqlSource((DynamicSqlSource) sqlSource);
        } else {
            throw new RuntimeException("无法处理该类型的SqlSource：" + sqlSource.getClass());
        }
        MetaObject msObject = SystemMetaObject.forObject(ms);
        msObject.setValue("sqlSource", pageSqlSource);

        // XXX 由于Count查询需要修改返回值，因此这里创建一个Count查询的 MappedStatement
        String id = ms.getId();
        MappedStatement countMappedStatement = MappedStatementUtils.newCountMappedStatement(ms);
        COUNT_MAPPED_STATEMENT_MAP.put(id, countMappedStatement);
    }

    /**
     * 用当前这个拦截器生成对目标target的代理
     * <p>
     * 返回值会直接被赋值给原先的对象
     *
     * @return 代理对象
     * @author 李远明
     * @version 2016-05-25 13:14:28
     * @version 2016-07-21 16:32:39 修改 拦截对象为 Executor
     */
    @Override
    public Object plugin(Object target) {
        // 当目标类是Executor类型时，才包装目标类，否者直接返回目标本身，减少目标被代理的次数。
        if (target instanceof Executor) {
            return Plugin.wrap(target, this);
        } else {
            return target;
        }
    }

    @Override
    public void setProperties(Properties properties) {
        // this.dbType = properties.getProperty("dbType");
        // // logger.info("PaginationInterceptor.setProperties: {}", dbType);
        //
        // if (StringUtils.isBlank(this.dbType)) {
        // throw new RuntimeException("请在MyBatis的配置文件（mybatisConfig.xml）中为当前拦截器设置dbType属性。");
        // }
    }

}
